using System;
using System.Collections.Generic;
using com.bodurov.SilverlightControls.XmlCodeEditor.Enums;
using com.bodurov.SilverlightControls.XmlCodeEditor.Utils;

namespace com.bodurov.SilverlightControls.XmlCodeEditor
{
    public class IterativeTokenizer : ITokenizer
    {
        private static readonly HashSet<char> ENTITYALLOWED = new HashSet<char>("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-#");
        private static readonly HashSet<char> END_OF_NAME = new HashSet<char>(" \t\r\n\b\"\'>/<=");
        private static readonly HashSet<char> SPACE_OR_OPEN_BRACKET = new HashSet<char>(" \t\r\n\b<");
        private static readonly HashSet<char> SPACE_OR_SERVER_SCRIPT_START = new HashSet<char>(" \t\r\n\b<%");

        private const string ServerScriptCodeStarts = "<%";
        private const string ServerScriptCodeEnds = "%>";
        private const string ClientSideCommentStarts = "<!--";
        private const string ClientSideCommentEnds = "-->";
        private const string ProcessingInstructionStarts = "<?";
        private const string ProcessingInstructionEnds = "?>";
        private const string ClosingTagBeginBracket = "</";
        private const string OpenTagBracket = "<";

        public List<ListOfTokens> ParseTokens(string value)
        {
            return ParseTokens(value, XmlArea.OutsideTag);
        }

        public List<ListOfTokens> ParseTokens(string value, XmlArea area)
        {
            if(value != null) value = value.Replace("\r", "");
            var state = new TokenizationState(value, 0, area);

            while(state.MoveToNextLine())
            {
                state = ParseLineSegment(state);
            }

            return state.LinesOfTokens;
        }


        private TokenizationState ParseLineSegment(TokenizationState state)
        {
            /*switch (state.LastElement)
            {
                case XmlElement.OpeningTagName:
                case XmlElement.ClosingTagName:
                case XmlElement.OpeningTagNamespace:
                case XmlElement.ClosingTagNamespace:
                case XmlElement.AttributeName:
                case XmlElement.AttributeEqualSign:
                case XmlElement.AttributeSingleQuoteString:
                case XmlElement.AttributeDualQuoteString:
                case XmlElement.AttributeLiteralContent:
                case XmlElement.OpeningProcessingInstructionBracket:
                case XmlElement.ClosingProcessingInstructionBracket:
                    state = ParseInsideTag(state, "/>", XmlElement.OpeningTagClosingBracket, XmlElement.OpeningTagEndBracket);
                    break;
                case XmlElement.InsideClientSideComment:
                case XmlElement.OpeningClientSideCommentBracket:
                    state = ParseInsideClientSideComment(state, ClientSideCommentEnds, XmlElement.ClosingClientSideCommentBracket);
                    break;
                //case XmlElement.OutsideTagContent:
                //case XmlElement.OpeningTagBeginBracket:
                //case XmlElement.OpeningTagEndBracket:
                //case XmlElement.OpeningTagClosingBracket:
                //case XmlElement.ClosingTagBeginBracket:
                //case XmlElement.ClosingTagEndBracket:
                //case XmlElement.OpeningServerScriptBracket:
                //case XmlElement.ServerBlockSpecialChar:
                //case XmlElement.ClosingServerScriptBracket:
                //case XmlElement.ClosingClientSideCommentBracket:
                //case XmlElement.Entity:
                //case XmlElement.WhiteSpace:
                //case XmlElement.InsideServerScript:
                //case XmlElement.EndOfFile:
                default:
                    state = ParseOutsideTag(state);
                    break;
            }*/
            switch (state.CurrentArea)
            {
                case XmlArea.Undefined:
                case XmlArea.OutsideTag:
                    state = ParseOutsideTag(state);
                    break;
                
                case XmlArea.InsideClientSideComment:
                //case XmlArea.InsideXmlCData:
                    state = ParseInsideClientSideComment(state, ClientSideCommentEnds, XmlElement.ClosingClientSideCommentBracket);
                    break;
                case XmlArea.InsideServerScriptCode:
                case XmlArea.InsideServerScriptPrintCode:
                case XmlArea.InsideServerBindStatement:
                    state = ParseInsideServerScript(state, ServerScriptCodeEnds, XmlElement.ClosingServerScriptBracket);
                    break;
                //case XmlArea.InsideTag:
                //case XmlArea.InsideProcessingInstruction:
                //case XmlArea.InsideSingleQuoteString:
                //case XmlArea.InsideDualQuoteString:
                //case XmlArea.InsideServerDeclaration:
                //case XmlArea.InsideServerBindStatement:
                //case XmlArea.InsideXmlComment:
                default:
                    state = ParseInsideTag(state, "/>", XmlElement.OpeningTagClosingBracket, XmlElement.OpeningTagEndBracket);
                    break;
            }
            if (state.CurrentArea == XmlArea.OutsideTag)
            {
                ParseOutsideTag(state);
            }
            state.LineHasBeenParsed();
            return state;
        }


        internal int GetIndexOfIfFollowedByWithin(TokenizationState state, char toFind, HashSet<char> allowedCharacters, int limit)
        {
            var count = 0;
            for (var i = state.CurrentCharIndex + 1; i < state.CurrentLine.Length; i++)
            {
                if (++count > limit) break;

                var curr = state.CurrentLine[i];
                if (curr == toFind) return i;
                if (!allowedCharacters.Contains(curr))
                {
                    return -1;
                }
            }
            return -1;
        }

        internal bool IsFollowedBySpace(TokenizationState state)
        {
            var next = state.CurrentCharIndex + 1;
            return state.CurrentLine.Length > next && Char.IsWhiteSpace(state.CurrentLine[next]);
        }

        internal bool AttemptToAddTagName(TokenizationState state, XmlElement type)
        {
            return AttemptToAddTagName(state, type, false);
        }

        internal bool AttemptToAddTagName(TokenizationState state, XmlElement type, bool allowSpaceBeforeName)
        {
            if (allowSpaceBeforeName)
            {
                this.AttemptToAddWhiteSpace(state);
            }
            var index = IndexOfAny(state, END_OF_NAME);
            if (index > 0 && index > state.CurrentCharIndex)
            {
                state.AddToken(type, state.CurrentCharIndex, index);
                return true;
            }
            return false;
        }

        internal bool AttemptToAddTagNamespace(TokenizationState state, XmlElement type)
        {
            var pair = IndexOfBeforeAnyOfLimiters(state, ':', END_OF_NAME);
            if (pair.First >= 0 && pair.Second >= 0 && pair.First < pair.Second)
            {
                state.AddToken(type, state.CurrentCharIndex, pair.First + 1);
                return true;
            }
            return false;
        }

        internal bool AttemptToAddWhiteSpace(TokenizationState state)
        {
            return AddTokenWhileCondition(state, Char.IsWhiteSpace, XmlElement.WhiteSpace);
        }

        private static bool AddTokenWhileCondition(TokenizationState state, Predicate<char> condition, XmlElement type)
        {
            if (!EndIsReached(state) && condition(state.CurrentChar))
            {
                var startIndex = state.CurrentCharIndex;
                do
                {
                    if (!state.MoveNext())
                    {
                        break;
                    }
                }
                while (condition(state.CurrentChar));
                state.AddToken(type, startIndex, state.CurrentCharIndex);
                return true;
            }
            return false;
        }

        private static bool EndIsReached(TokenizationState state)
        {
            var b = state.CurrentCharIndex >= state.CurrentLine.Length;
            if (b) state.EndHasBeenReached = true;
            return b;
        }

        internal bool IsEndOfTag(TokenizationState state, string expectedTerminator)
        {
            if (EndIsReached(state)) return true;
            this.AttemptToAddWhiteSpace(state);
            if (EndIsReached(state)) return true;
            if (state.CurrentChar == '<' || state.CurrentChar == '>' || this.CurrentCharSequenceIs(state, "/>") || 
                (expectedTerminator != null && this.CurrentCharSequenceIs(state, expectedTerminator)))
            {
                return true;
            }
            return false;
        }

        internal int IndexOfAny(TokenizationState state, HashSet<char> toFind)
        {
            var i = state.CurrentCharIndex;
            for (; i < state.CurrentLine.Length; i++)
            {
                if (toFind.Contains(state.CurrentLine[i]))
                {
                    break;
                }
            }
            return i;
        }

        internal PairStruct<int, int> IndexOfBeforeAnyOfLimiters(TokenizationState state, char search, HashSet<char> limiters)
        {
            var i = state.CurrentCharIndex;
            var first = -1;
            var second = -1;
            for (; i < state.CurrentLine.Length; i++)
            {
                var ch = state.CurrentLine[i];
                if (ch == search) first = i;
                if (limiters.Contains(ch))
                {
                    break;
                }
            }
            second = i;

            return new PairStruct<int, int> { First = first, Second = second };
        }

        internal bool AttemptToAddAttribute(TokenizationState state, string expectedTerminator)
        {
            if (IsEndOfTag(state, expectedTerminator)) return false;
            //var canAddNamespace = this.AttemptToAddAttributeNamespace(state);
            var canAddName = this.AttemptToAddAttributeName(state);
            if (IsEndOfTag(state, expectedTerminator)) return false;
            var canAddSign = this.AttemptToAddEqualSign(state);
            if (!canAddSign && canAddName)
            {
                return true;
            }
            if (IsEndOfTag(state, expectedTerminator)) return false;
            var canAddAttributeContent = this.AttemptToAddAttributeContent(state);
            return canAddName || canAddAttributeContent;
        }
        internal bool AttemptToAddAttributeName(TokenizationState state)
        {
            var index = IndexOfAny(state, END_OF_NAME);
            if (index > state.CurrentCharIndex)
            {
                state.AddToken(XmlElement.AttributeName, state.CurrentCharIndex, index);
                return true;
            }
            return false;
        }
        internal bool AttemptToAddEqualSign(TokenizationState state)
        {
            if (state.CurrentChar == '=')
            {
                state.AddToken(XmlElement.AttributeEqualSign, state.CurrentCharIndex, state.CurrentCharIndex + 1);
                state.MoveNext();
                return true;
            }
            return false;
        }
        internal bool AttemptToAddAttributeContent(TokenizationState state)
        {
            var canAddDualQuotes = this.AttemptToAddDualQuoteString(state);
            if (!canAddDualQuotes)
            {
                var canAddSingleQuotes = this.AttemptToAddSingleQuoteString(state);
                if (!canAddSingleQuotes)
                {
                    var canAddLiteralContrent = this.AttemptToLiteralAttributeContent(state);
                    if (!canAddLiteralContrent)
                    {
                        return false;
                    }
                }
            }
            return true;
        }
        private static QuoteSearchState GetStringEndIndex(TokenizationState state, int i, char quote)
        {
            var skipNext = false;
            var shift = 0;
            var isEndQuoteFound = false;
            for (; i < state.CurrentLine.Length; i++)
            {
                if (skipNext)
                {
                    skipNext = false;
                    continue;
                }
                var ch = state.CurrentLine[i];
                if (ch == '\\') // if this is escape char so skip the next char
                {
                    skipNext = true;
                    continue;
                }
                if (ch == quote)
                {
                    isEndQuoteFound = true;
                    shift++;
                    break;
                }
                if (ch == '>' && state.CurrentLineHasOnlySpaceAfterIndex(i))
                {
                    break;
                }
            }
            return new QuoteSearchState { EndIndex = i + shift, IsEndQuoteFound = isEndQuoteFound };
        }
        internal bool AttemptToAddQuotedString(TokenizationState state, char quote, XmlElement element)
        {
            if (state.CurrentChar != quote) return false;
            var i = state.CurrentCharIndex + 1;//skip the quote character
            var quoteSearchState = GetStringEndIndex(state, i, quote);
            state.AddToken(element, state.CurrentCharIndex, quoteSearchState.EndIndex);
            // if the end quote is not found - search more lines
            if (!quoteSearchState.IsEndQuoteFound)
            {
                ParseMoreLines(state, new PairStruct<XmlElement, char> { First = element, Second = quote },
                        (s, d) =>
                        {
                            var qss = GetStringEndIndex(s, s.CurrentCharIndex, d.Data.Second);
                            state.AddToken(d.Data.First, s.CurrentCharIndex, qss.EndIndex);
                            
                            // if the end quote is found - no need to search more lines, so just exit
                            if (qss.IsEndQuoteFound) d.Cancel = true;
                        });
            }

            return true;
        }
        internal bool AttemptToAddSingleQuoteString(TokenizationState state)
        {
            return AttemptToAddQuotedString(state, '\'', XmlElement.AttributeSingleQuoteString);
        }
        internal bool AttemptToAddDualQuoteString(TokenizationState state)
        {
            return AttemptToAddQuotedString(state, '"', XmlElement.AttributeDualQuoteString);
        }
        internal bool AttemptToLiteralAttributeContent(TokenizationState state)
        {
            return AddTokenWhileCondition(state, ch => "<> \t\n\r\b/".IndexOf(ch) < 0, XmlElement.AttributeLiteralContent);
        }


        internal bool CurrentCharSequenceIs(TokenizationState state, string str)
        {
            return state.CurrentLine.IndexOf(str, state.CurrentCharIndex) == state.CurrentCharIndex;
        }

        internal bool AttemptToAddEntity(TokenizationState state)
        {
            var endIndex = this.GetIndexOfIfFollowedByWithin(state, ';', ENTITYALLOWED, 16);
            if (endIndex >= 0)
            {
                var ampIndex = state.CurrentCharIndex;
                // if there is something between the current and the last elements add it as outside content
                if (ampIndex > state.LastCharIndex)
                {
                    state.AddToken(XmlElement.OutsideTagContent, state.LastCharIndex, ampIndex);
                }
                state.AddToken(XmlElement.Entity, ampIndex, endIndex + 1);
                return true;
            }
            return false;
        }

        internal bool AttemptToAddAnyTag(TokenizationState state)
        {
            return !AttemptToAddClientSideComment(state)
                       ? // <!-- ... -->
                   !AttemptToAddProcessingInstruction(state)
                       ? // <? ... ?>
                   !AttemptToAddServerScript(state)
                       ? // <% ... %>
                   !AttemptToAddClosingTag(state)
                       ? // </ ... >
                   !AttemptToAddTag(state)
                       ? // < ... >
                   
                   false
                       : true
                       : true
                       : true
                       : true
                       : true;
                                    
        }

        internal bool AttemptToAddServerScript(TokenizationState state)
        {
            if (this.CurrentCharSequenceIs(state, ServerScriptCodeStarts)) // <%
            {
                AttemptToAddContentBeforeTheTag(state);

                var ch = GetFirstCharThatIsNotWithin(state, SPACE_OR_SERVER_SCRIPT_START);
                var area = ch == '='
                               ? XmlArea.InsideServerScriptPrintCode
                               : ch == '#'
                                     ? XmlArea.InsideServerBindStatement
                                     : ch == '@'
                                           ? XmlArea.InsideServerDeclaration
                                           : XmlArea.InsideServerScriptCode;

                state.CurrentArea = area;
                state.AddToken(XmlElement.OpeningServerScriptBracket, ServerScriptCodeStarts);
                if (area != XmlArea.InsideServerScriptCode) AttemptToAddSpecialServerChar(state);
                if (area == XmlArea.InsideServerDeclaration)
                {
                    this.AttemptToAddTagName(state, XmlElement.OpeningTagName, true);
                    ParseInsideTag(state, ServerScriptCodeEnds, XmlElement.ClosingServerScriptBracket, XmlElement.ClosingServerScriptBracket);
                }
                else ParseInsideServerScript(state, ServerScriptCodeEnds, XmlElement.ClosingServerScriptBracket); // %>
                return true;
            }
            return false;
        }

        internal bool AttemptToAddSpecialServerChar(TokenizationState state)
        {
            AttemptToAddWhiteSpace(state);
            if(state.CurrentChar == '=' || state.CurrentChar == '#' || state.CurrentChar == '@')
            {
                state.AddToken(XmlElement.ServerBlockSpecialChar, state.CurrentChar.ToString());
                return true;
            }
            return false;
        }

        private static char GetFirstCharThatIsNotWithin(TokenizationState state, HashSet<char> within)
        {
            for (var i = state.CurrentCharIndex; i < state.CurrentLine.Length; i++)
            {
                if (!within.Contains(state.CurrentLine[i]))
                {
                    return state.CurrentLine[i];
                }
            }
            return state.CurrentLine[state.CurrentLine.Length - 1];
        }

        internal bool AttemptToAddProcessingInstruction(TokenizationState state)
        {
            if (this.CurrentCharSequenceIs(state, ProcessingInstructionStarts)) // <?
            {
                AttemptToAddContentBeforeTheTag(state);
                state.CurrentArea = XmlArea.InsideProcessingInstruction;
                state.AddToken(XmlElement.OpeningProcessingInstructionBracket, ProcessingInstructionStarts);
                this.AttemptToAddTagName(state, XmlElement.OpeningTagName, true);
                ParseInsideTag(state, ProcessingInstructionEnds, XmlElement.ClosingProcessingInstructionBracket, XmlElement.ClosingProcessingInstructionBracket); // ?>
                return true;
            }
            return false;
        }
        internal bool AttemptToAddClientSideComment(TokenizationState state)
        {
            if (this.CurrentCharSequenceIs(state, ClientSideCommentStarts)) // <!--
            {
                AttemptToAddContentBeforeTheTag(state);
                state.CurrentArea = XmlArea.InsideClientSideComment;
                state.AddToken(XmlElement.OpeningClientSideCommentBracket, ClientSideCommentStarts);
                ParseInsideClientSideComment(state, ClientSideCommentEnds, XmlElement.ClosingClientSideCommentBracket); // -->
                return true;
            }
            return false;
        }
        internal bool AttemptToAddClosingTag(TokenizationState state)
        {
            if (this.CurrentCharSequenceIs(state, ClosingTagBeginBracket)) // </
            {
                AttemptToAddContentBeforeTheTag(state);
                state.CurrentArea = XmlArea.InsideTag;
                state.AddToken(XmlElement.ClosingTagBeginBracket, ClosingTagBeginBracket);
                this.AttemptToAddTagNamespace(state, XmlElement.ClosingTagNamespace);
                this.AttemptToAddTagName(state, XmlElement.ClosingTagName);
                ParseInsideTag(state, ">", XmlElement.ClosingTagEndBracket, XmlElement.ClosingTagEndBracket);
                return true;
            }
            return false;
        }
        internal bool AttemptToAddTag(TokenizationState state)
        {
            if (state.CurrentChar == '<' && !this.IsFollowedBySpace(state)) // <
            {
                AttemptToAddContentBeforeTheTag(state);
                state.CurrentArea = XmlArea.InsideTag;
                state.AddToken(XmlElement.OpeningTagBeginBracket, OpenTagBracket);
                this.AttemptToAddTagNamespace(state, XmlElement.OpeningTagNamespace);
                this.AttemptToAddTagName(state, XmlElement.OpeningTagName);
                ParseInsideTag(state, "/>", XmlElement.OpeningTagClosingBracket, XmlElement.OpeningTagEndBracket);
                return true;
            }
            return false;
        }

        private static bool AttemptToAddContentBeforeTheTag(TokenizationState state)
        {
            if(state.LastCharIndex < state.CurrentCharIndex)
            {
                state.AddToken(XmlElement.OutsideTagContent, state.LastCharIndex, state.CurrentCharIndex);
                return true;
            }
            return false;
        }

        private TokenizationState ParseOutsideTag(TokenizationState state)
        {
            while (state.MoveNext())
            {
                //Dbg.Trace("state.CurrentChar={0}", state.CurrentChar);
                if (state.CurrentChar == '&')
                {
                    AttemptToAddEntity(state);
                }
                else if(state.CurrentChar == '<')
                {
                    if (HasNextCharAndIsOneOf(state, SPACE_OR_OPEN_BRACKET))
                    {
                        continue;
                    }

                    if(AttemptToAddAnyTag(state))
                    {
                        if (state.CurrentArea == XmlArea.OutsideTag)// if end of the line is not reached by the end of line
                        {
                            ParseOutsideTag(state);
                        }
                    }
                }
            }

            var numChars = state.CurrentLine.Length;
            if (state.LastCharIndex != numChars)
            {
                state.AddToken(XmlElement.OutsideTagContent, state.LastCharIndex, numChars);
            }

            return state;
        }

        private static bool HasNextCharAndIsOneOf(TokenizationState state, HashSet<char> chars)
        {
            if (state.CurrentCharIndex >= (state.CurrentLine.Length - 1)) return false;
            return chars.Contains(state.CurrentLine[state.CurrentCharIndex + 1]);
        }


        private TokenizationState ParseInsideTag(TokenizationState state, string terminator, XmlElement endingBracketIfTerminatorFound, XmlElement endingBracketElse)
        {
            while (AttemptToAddAttribute(state, terminator))
            {
            }
            if (EndIsReached(state)) return state;
            var terminatorFound = CurrentCharSequenceIs(state, terminator);
            var len = terminatorFound ? terminator.Length : 1;
            if (state.CurrentChar == '<')
            {
                return ParseOutsideTag(state);
            }
            if (terminatorFound || state.CurrentChar == '>')
            {
                var endingBracket = terminatorFound ? endingBracketIfTerminatorFound : endingBracketElse;
                state.AddToken(endingBracket, state.CurrentCharIndex, state.CurrentCharIndex + len); // add the closing bracket token
                state.CurrentArea = XmlArea.OutsideTag;
            }
            return state;
        }

        private static TokenizationState ParseInsideServerScript(TokenizationState state, string terminator, XmlElement endingBracket)
        {
            return ParseInsideArea(state, XmlElement.InsideServerScript, terminator, endingBracket);
        }

        private static TokenizationState ParseInsideClientSideComment(TokenizationState state, string terminator, XmlElement endingBracket)
        {
            return ParseInsideArea(state, XmlElement.InsideClientSideComment, terminator, endingBracket);
        }

        private static ParseMoreLinesState<T> ParseMoreLines<T>(TokenizationState state, T data, Action<TokenizationState, ParseMoreLinesState<T>> parse)
        {
            if(state.CanMoveToNextLine())
            {
                state.LineHasBeenParsed();
            }
            var val = new ParseMoreLinesState<T> { Data = data, Cancel = false };
            while (state.MoveToNextLine())
            {
                parse(state, val);

                state.LineHasBeenParsed();
                if (val.Cancel) break;
            }
            return val;
        }

        private static TokenizationState ParseInsideArea(TokenizationState state, XmlElement area, string terminator, XmlElement endingBracket)
        {
            var index = state.CurrentLine.IndexOf(terminator, state.CurrentCharIndex);
            var hasTerminator = index >= 0;
            if (index < 0) index = state.CurrentLine.Length;
            
            if(state.CurrentCharIndex < index)
                state.AddToken(area, state.CurrentCharIndex, index);

            if(index == state.CurrentLine.Length)
            {
                var pmls = 
                    ParseMoreLines(state, new PairStruct<XmlElement, string> {First = area, Second = terminator}, 
                    (s, d)=>
                            {
                                var idx = s.CurrentLine.IndexOf(d.Data.Second, s.CurrentCharIndex);
                                d.Cancel = idx >= 0;
                                if (idx < 0) idx = s.CurrentLine.Length;

                                if (s.CurrentCharIndex < idx)
                                    s.AddToken(d.Data.First, s.CurrentCharIndex, idx);
                            });
                if (pmls.Cancel) hasTerminator = true;
            }

            if (hasTerminator)
            {
                state.AddToken(endingBracket, state.CurrentCharIndex, state.CurrentCharIndex + terminator.Length);
                state.CurrentArea = XmlArea.OutsideTag;
            }
            return state;
        }

        class ParseMoreLinesState<T>
        {
            public bool Cancel { get; set; }
            public T Data { get; set; }
        }

        struct QuoteSearchState
        {
            public int EndIndex { get; set; }
            public bool IsEndQuoteFound { get; set; }
        }
    }
}